feat(wallet): Add split persistence backed by better-sqlite3#8480
feat(wallet): Add split persistence backed by better-sqlite3#8480FrederikBolding merged 21 commits intofeat/wallet-libraryfrom
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
This comment was marked as resolved.
This comment was marked as resolved.
|
@SocketSecurity ignore npm/better-sqlite3@11.10.0 Yes, it has native binaries. |
|
@SocketSecurity ignore-all All alerts are due to |
8f58b8c to
f66328d
Compare
623f9af to
3fb60ee
Compare
1d6d1fc to
937ba80
Compare
937ba80 to
29c6ac6
Compare
0cf2d0d to
520a971
Compare
45d013e to
6ec54b9
Compare
Persist controller state to SQLite using a per-property key-value scheme (one row per ControllerName.propertyName). Writes are synchronous within the same call stack as controller state updates, using stateChanged events and Immer patches to write only changed, persist-flagged properties. Defaults to ':memory:' when no database path is provided. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The build tsconfig is stricter than the dev tsconfig — the union type of controller instances does not expose the protected `destroy` method. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review findings: add constructor cleanup on failure, make destroy() idempotent with Promise.allSettled and finally, handle remove patches via store.delete(), catch and log handler write failures, validate degenerate store keys, handle root state replacement patches, add contextful JSON.parse errors, tighten loadState return type, extract PersistableController type and storeKey utility, remove premature KeyValueStore public export, and add tests for new behaviors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
520a971 to
cfad387
Compare
Make `@metamask/wallet` platform-agnostic by removing the SQLite-backed `KeyValueStore` from the `Wallet` constructor. `Wallet` now accepts a flat `WalletOptions` (including an optional `state` payload) and owns no persistence state. Consumers arrange persistence themselves. - `Wallet` exposes a `controllerMetadata` getter so persistence can operate on metadata alone, without reaching into controller instances. - A new `Root:walletDestroyed` event is published after controllers tear down during `destroy()`. `subscribeToChanges` listens for it and self-unsubscribes, eliminating the need for callers to hold an unsubscribe handle. - `subscribeToChanges` now takes a `Record<string, StateMetadataConstraint>` instead of the controller-instance map, and specializes its messenger type to `RootMessenger<DefaultActions, DefaultEvents>` so subscribing to `Root:walletDestroyed` type-checks without suppression. - `KeyValueStore`, `loadState`, and `subscribeToChanges` are exposed via a new `@metamask/wallet/persistence` subpath export; they'll move to their own package later.
So that @metamask/wallet/persistence can be moved to its own package without needing to deep-import from the wallet package.
06b91f1 to
f4e8dfa
Compare
Renames install-anvil.sh to install-binaries.sh and adds a better-sqlite3 native addon rebuild step for CI environments where the prebuilt binding is absent or incompatible with the Node version. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Check for the compiled addon at `node_modules/better-sqlite3/build/Release/better_sqlite3.node` and skip `prebuild-install` when it's already present. Avoids unnecessary network calls on every `test:prepare` run. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4c16ea9. Configure here.
| const changed = getChangedProperties(patches, persistedProperties); | ||
|
|
||
| for (const prop of changed) { | ||
| const key = storeKey(controllerName, prop); |
There was a problem hiding this comment.
The clients do persistence using KV where the key is the controller name. Do we think it is preferable to do at the property level? If so, why?
There was a problem hiding this comment.
It's faster. SQLite stores a text representation of the JSON values under the hood. Every time we modify a value stored at a given key in the state table, we overwrite the value of that key with JSON.stringify(newValue) (see KeyValueStore.ts). Consequently, by splitting controller state across multiple keys, there is less persistence overhead. There are ways to make this vastly more efficient, but this should be fine for know.
I don't know what the tradeoffs look like for this in the browser or React Native.
| persistedProperties: ReadonlySet<string>, | ||
| ): ReadonlySet<string> { | ||
| const changed = new Set<string>(); | ||
| for (const patch of patches) { |
There was a problem hiding this comment.
Generally we try to not inspect the patches as it has proven error-prone, it may be fine in this case, but flagging regardless.
There was a problem hiding this comment.
I hear that, although in this case we're not deeply inspecting the patches but relying on a couple of invariants:
- If the patch path length is 0, the entire value has been replaced
- The first element of the path array is the name of the first modified property

Summary
persist: truemetadata gets its own row in akvtable (key format:ControllerName.propertyName)controller.update()viastateChangedevent subscriptions, eliminating data loss windows:memory:when no database path is providedTest plan
yarn workspace @metamask/wallet exec jest --no-coverage --watchman=false src/persistence/— 22 unit tests covering KeyValueStore CRUD, loadState grouping, persist filtering, StateDeriver application, patch-based diffing, unsubscribe, and multi-controller scenariosyarn workspace @metamask/wallet exec tsc --noEmit— no new type errors🤖 Generated with Claude Code
Note
Medium Risk
Introduces a new persistence layer and changes wallet lifecycle/messenger semantics, plus adds a native dependency (
better-sqlite3) that can affect build/install reliability across environments.Overview
Adds a new
persistencesubpath export that provides synchronous SQLite-backed state storage viaKeyValueStore, plusloadState(reconstruct controller state fromController.propkeys) andsubscribeToChanges(persist onlypersist-flagged controller properties based on Immer patches and delete on removal).Updates
Walletconstruction/typing to accept optional preloadedstate, exposescontrollerMetadatafor initialized controllers, switches messenger namespace toWallet, and makesdestroy()idempotent while publishing a newWallet:destroyedevent after best-effort controller teardown.Wires in the native
better-sqlite3dependency (with LavaMoat allow-scripts), adds test/CI setup to install required binaries (anvil+better-sqlite3prebuild) and documents how to rebuild the native addon.Reviewed by Cursor Bugbot for commit 772c1e6. Bugbot is set up for automated code reviews on this repo. Configure here.